<?php

/**
 * 2010-11-29 Google changes API URLs spec. so you can use 'Usage 1' only.
 * 2010-07-30 Google changes json spec.
 *
 * getting your app's stats on Developer Console(Android Market) 
 * 
 * 
 * - Usage 1:
 * -- get stats data(JSON)
 * 
 * try {
 *     //$email: email address for Developer Console log-in
 *     //$password: password for Developer Console log-in
 * 
 *     $ams = new AndroidAppStats($email, $password);
 *     
 *     echo($ams->json());
 *
 * } catch (AndroidAppStatsException $e) {
 *     //exception
 * } catch (Exception $e) {
 *     //exception
 * }
 * 
 * 
 * - Usage 2:
 * -- using package name as key of associative array, then values are stats data.
 * 
 * try {
 *     //$email:    email address for Developer Console log-in
 *     //$password: password for Developer Console log-in
 *     //$package_prefix: package name prefix of app. 
 *     //  if you uploaded "com.example.android.app1" and "com.example.android.app2", $package_prefix is "com.example.android." (NOT "com.example.android")
 * 
 *     $ams = new AndroidAppStats($email, $password);
 *     $ams->setPackagePrefix($package_prefix);
 * 
 *     var_dump($ams->stats());
 *
 * } catch (AndroidAppStatsException $e) {
 *     //exception
 * } catch (Exception $e) {
 *     //exception
 * }
 * 
 * - Usage 3:
 * -- using sqlite and insert stats data into sqlite database.
 * 
 * try {
 *     //$email:    email address for Developer Console log-in
 *     //$password: password for Developer Console log-in
 *     //$package_prefix: package name prefix of app. 
 *     //  if you uploaded "com.example.android.app1" and "com.example.android.app2", $package_prefix is "com.example.android." (NOT "com.example.android")
 *     //$dir_this_script: /directory/path/to/this/script
 * 
 *     $ams = new AndroidAppStats($email, $password);
 *     $ams->setPackagePrefix($package_prefix);
 *     $ams->setDirScript($dir_this_script);
 * 
 *     $ams->cron();
 * 
 * } catch (AndroidAppStatsException $e) {
 *     //exception
 * } catch (PDOException $e) {
 *     //exception
 * } catch (Exception $e) {
 *     //exception
 * }
 * 
 */
class AndroidAppStats{

    private $permutation;

    private $postData;

    private $dsn;

    private $sqlite;

    private $cookie;

    private $log;

    private $email;
    
    private $password;

    private $json;

    private $appPackagePrefix;

    private $dateTime;

    private $clazz;

    private $stats;

    private $issetDirScript;
    
    private $nolog;
    
    private $pdo;

    const USER_AGENT = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.70 Safari/533.4";

    public function __construct($email, $password) {
        ini_set("user_agent", self::USER_AGENT);

        $this->email = $email;
        $this->password = $password;

        $this->permutation = null;
        $this->postData = null;
        $this->json = null;
        $this->stats = array();
        $this->nolog = false;

        $this->clazz = strtolower(__CLASS__);
        
        $dirScript = getcwd();
        $this->cookie = "{$dirScript}/{$this->clazz}.cookie";
        $this->log = "{$dirScript}/{$this->clazz}.log";
        
        $this->dateTime = date("Y-m-d H:i:s");

        $this->pdo = null;
        $this->dsn = null;
        $this->appPackagePrefix = null;
        $this->issetDirScript = false;
        $this->sqlite = null;
    }

    private function log($message) {
        if (!$this->nolog) {
            @file_put_contents($this->log, date("Y-m-d H:i:s") . "\t{$message}".PHP_EOL, FILE_APPEND);
        }
    }
    
    public function nolog() {
        $this->nolog = true;
    }

    public function setPackagePrefix($appPackagePrefix) {
        $this->appPackagePrefix = $appPackagePrefix;
    }
    
    public function setDirScript($dirScript) {
        
        $this->sqlite = "{$dirScript}/{$this->clazz}.sqlite";
        $this->cookie = "{$dirScript}/{$this->clazz}.cookie";
        $this->log = "{$dirScript}/{$this->clazz}.log";
        
        $this->issetDirScript = true;    
    }

    /**
     * using sqlite and insert stats data into sqlite database.
     * useful for crontab
     */
    public function cron() {
        
        if (!$this->issetDirScript) {
            throw new AndroidAppStatsException("setDirScript() was not called");
        }
        
        $this->dsn = "sqlite:{$this->sqlite}";
        
        //create sqlite file
        if (!is_file($this->sqlite)) {
            $this->initDatabase();
        }
        
        $this->pdo = new PDO($this->dsn);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        $this->permutation = null;
        $this->postData = null;
        $this->json = null;

        $this->login();

        $sql = "SELECT x_gwt_permutation, post_data FROM stats_info LIMIT 1;";
        $stmt = $this->pdo->query($sql);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        $stmt->closeCursor();
        
        if (empty($row["x_gwt_permutation"]) || empty($row["post_data"])) {
            $this->retrieve();
        } else {            
            if (!$this->getStats($row["x_gwt_permutation"], $row["post_data"])) {
                $this->retrieve();
            }
        }
        
        if (empty($this->permutation) || empty($this->postData) || empty($this->json)) {
            throw new AndroidAppStatsException("empty stats data");
        } else {
            $this->parseJson();
            $this->saveStatsInfo();
            $this->saveStatsData();
        }
        
        return true;
    }
    
    /**
     * return raw stats data(JSON)
     */
    public function json() {
        $this->login();
        $this->retrieve();
        return $this->json;
    }
    
    /**
     * using package name as key of associative array, then values are stats data.
     */
    public function stats() {
        $this->login();
        $this->retrieve();
        $this->parseJson();
        return $this->stats;
    }

    /**
     * login to Google Account
     */
    private function login() {
        
        if (empty($this->email) || empty($this->password) || empty($this->cookie)) {
            throw new AndroidAppStatsException("invalid login params");
        }

        //get GALX
        $ch = curl_init("https://www.google.com/accounts/ServiceLogin");
        curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie);

        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);

        $html = curl_exec($ch);
        curl_close($ch);

        $html = preg_replace("/\s/", "", $html);

        $match = array();
        preg_match("/name=\"GALX\"value=\"(.+?)\"\/>/", $html, $match);

        if (empty($match[1])) {
            throw new AndroidAppStatsException("cannot get GALX");
        } else {
            $galx = trim($match[1]);
        }

        //ServiceLoginAuth(Google)
        $ch = curl_init("https://www.google.com/accounts/ServiceLoginAuth");
        curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie);

        $params = array(
            "service"    => "androiddeveloper",
            "GALX"        => $galx,
            "Email"        => $this->email,
            "Passwd"    => $this->password,
            "continue"    => "http://market.android.com/publish"
        );

        //CURLOPT_POST, CURLOPT_POSTFIELDS
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $params);

        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);

        $html = curl_exec($ch);
        curl_close($ch);

        $match = array();
        preg_match("/url=&#39;(.+?)&#39;/", $html, $match);

        if (empty($match[1])) {
            throw new AndroidAppStatsException("cannot get redirect");
        } else {
            $redirect = trim($match[1]);
        }

        $redirect = preg_replace("/&amp;/", "&", $redirect);

        $ch = curl_init($redirect);
        curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie);
        curl_setopt($ch, CURLOPT_POST, FALSE);

        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);

        curl_exec($ch);
        curl_close($ch);
    }

    private function getJson($permutation, $postData) {
        
        if (empty($permutation) || empty($postData)) {
            throw new AndroidAppStatsException("cannot get json");
        }

        $url = "http://market.android.com/publish/editapp";
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie);

        //POST
        curl_setopt($ch, CURLOPT_POST, TRUE);

        //2010-11-29
        //curl_setopt($ch, CURLOPT_POSTFIELDS, "5|0|4|http://market.android.com/publish/gwt/|{$postData}|com.google.wireless.android.vending.developer.shared.AppEditorService|getFullAssetInfosForUser|1|2|3|4|0|");
        curl_setopt($ch, CURLOPT_POSTFIELDS, "6|0|6|http://market.android.com/publish/gwt/|{$postData}|com.google.wireless.android.vending.developer.shared.AppEditorService|getFullAssetInfosForUser|java.lang.String/2004016611|J|1|2|3|4|3|5|6|6|0|A|J|");

        $headers = array(
            "Content-Type: text/x-gwt-rpc; charset=utf-8",
            "X-GWT-Permutation: {$permutation}",
            "X-GWT-Module-Base: http://market.android.com/publish/gwt/"
        );
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);

        $json = curl_exec($ch);
        curl_close($ch);

        return $json;
    }

    private function parseJson() {
        
        if (empty($this->appPackagePrefix)) {
            throw new AndroidAppStatsException("package prefix is null");
        }

        //2010-07-30 add
        $stats = json_decode($this->json);
        if (empty($stats)) {
            throw new AndroidAppStatsException("json_decode() failed");
        }

        $app_count = array();
        $app_info = array();
        $tmp = array();
        foreach ($stats as $stat) {
            if (is_array($stat)) {
                foreach ($stat as $info) {
                    if (strpos($info, $this->appPackagePrefix) !== false) {
                        $app_info[] = $info;
                    }
                }
            } else if (is_float($stat)) {
                $tmp[] = $stat;

                if (count($tmp) !== 0 && (count($tmp) % 7) === 0) {
//                    if ($tmp[6] === 0) {
//                        $percent = 0;
//                    } else {
//                        $percent = round($tmp[4]/$tmp[6], 2) * 100;
//                    }
//                    $app_count[] = array("ratings" => $tmp[1], "star" => round($tmp[2], 1), "total" => $tmp[6], "active" => $tmp[4], "percent" => $percent);

                    $app_count[] = array("ratings" => $tmp[1], "star" => round($tmp[2], 1), "total" => $tmp[6], "active" => $tmp[4]);
                    $tmp = array();
                }
            } else {

            }
        }

        rsort($app_info);

        if (count($app_info) !== count($app_count)) {
            throw new AndroidAppStatsException("stats does not match");
        }

        $this->stats = array_combine($app_info, $app_count);
    }

    private function getPostDataList($permutation) {
    
        //2010-11-29
        //$url = "http://market.android.com/publish/gwt/{$permutation}.cache.html";
        $url = "http://market.android.com/publish/gwt/{$permutation}.cache.js";        
        
        $html = @file_get_contents($url);

        $match = array();
        preg_match_all("/'([A-Z0-9]{32})'/", $html, $match, PREG_PATTERN_ORDER);

        $postDataList = array();
        if (empty($match[1])) {
            throw new AndroidAppStatsException("cannot get post_data");
        } else {
            return $match[1];
        }

    }


    private function getStats($permutation, $postData) {
        $json = $this->getJson($permutation, $postData);

        if ($this->isValidJson($json)) {
            $this->permutation = $permutation;
            $this->postData = $postData;
            $this->json = substr($json, 4);
            
            $this->log("{$permutation}/{$postData}");
            
            return true;
        } else {
            return false;
        }
    }

    private function retrieve() {
        $permutations = $this->getPermutations();

        foreach ($permutations as $permutation) {

            $postDataList = $this->getPostDataList($permutation);

            foreach ($postDataList as $postData) {
                if ($this->getStats($permutation, $postData)) {
                    return true;
                } else {
                    //do not "F5 attack"
                    sleep(2);
                }
            }
        }
        
        throw new AndroidAppStatsException("cannot retreive stats");
    }

    private function isValidJson($json) {
        if (substr($json, 0, 4) === "//OK") {
            return true;
        } else {
            return false;
        }
    }

    private function getPermutations() {

        $html = @file_get_contents("http://market.android.com/publish/gwt/com.google.wireless.android.vending.developer.HomeMod.nocache.js");

        $match = array();
        preg_match_all("/'([A-Z0-9]{32})'/", $html, $match, PREG_PATTERN_ORDER);

        if (isset($match[1]) && !empty($match[1])) {
            return $match[1];
        } else {
            throw new AndroidAppStatsException("cannot get permutations");
        }

    }

    private function initDatabase() {
        
        $pdo = null;
        try {

            @unlink($this->sqlite);
            $pdo = new PDO($this->dsn);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

            $pdo->beginTransaction();

            $sql = "CREATE TABLE stats_info (x_gwt_permutation TEXT NOT NULL, post_data TEXT NOT NULL, insert_dtime TEXT NOT NULL);";
            $pdo->query($sql);

            $sql = "CREATE TABLE stats_data (
                _id INTEGER PRIMARY KEY AUTOINCREMENT,
                ratings INTEGER NOT NULL,
                star TEXT NOT NULL,
                total INTEGER NOT NULL,
                active INTEGER NOT NULL,
                insert_dtime TEXT NOT NULL,
                package TEXT NOT NULL,
                hash TEXT UNIQUE NOT NULL
            );";
            $pdo->query($sql);

            $sql = "CREATE INDEX idx_package ON stats_data(package);";
            $pdo->query($sql);

            $sql = "CREATE INDEX idx_insert_dtime ON stats_data(insert_dtime);";
            $pdo->query($sql);

            $pdo->commit();
        } catch (PDOException $e) {
            $pdo->rollBack();
            $pdo = null;
            throw $e;
        } catch (Exception $e) {
            $pdo->rollBack();
            $pdo = null;
            throw $e;
        }

    }

    private function saveStatsData() {
        try {
            $this->pdo->beginTransaction();
            $stmt = null;

            foreach ($this->stats as $package => $stats) {

                //$hash is UNIQUE key, do not change this value.
                $hash = sha1(sprintf("%010d", $stats["ratings"]) . sprintf("%010d", $stats["total"]) . sprintf("%010d", $stats["active"]) . $stats["star"] . $package);

                $sql = "SELECT count(_id) FROM stats_data WHERE hash = '{$hash}';";
                $res = $this->pdo->query($sql);
                $count = $res->fetchColumn();
                $res->closeCursor();
                if ($count == 0) {
                    $prepare = "INSERT INTO stats_data (ratings, star, total, active, insert_dtime, package, hash) VALUES (:ratings, :star, :total, :active, '{$this->dateTime}', :package, :hash);";
                    $stmt = $this->pdo->prepare($prepare);
                    $stmt->execute(array(":ratings" => $stats["ratings"], ":star" => $stats["star"], ":total" => $stats["total"], ":active" => $stats["active"], ":package" => $package, ":hash" => $hash));
                    $stmt->closeCursor();
                }
            }
            $this->pdo->commit();
        } catch (PDOException $e) {
            $this->pdo->rollBack();
            $stmt = null;
            $this->pdo = null;
            throw $e;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            $stmt = null;
            $this->pdo = null;
            throw $e;
        }

    }
    
    private function saveStatsInfo() {
        try {
            $this->pdo->beginTransaction();
            $sql = "DELETE FROM stats_info;";
            $this->pdo->query($sql);

            $stmt = null;
            $prepare = "INSERT INTO stats_info (x_gwt_permutation, post_data, insert_dtime) VALUES (:x_gwt_permutation, :post_data, '{$this->dateTime}');";
            $stmt = $this->pdo->prepare($prepare);

            $stmt->execute(array(":x_gwt_permutation" => $this->permutation, ":post_data" => $this->postData));
            $stmt->closeCursor();

            $this->pdo->commit();
        } catch (PDOException $e) {
            $this->pdo->rollBack();
            $stmt = null;
            $this->pdo = null;
            throw $e;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            $stmt = null;
            $this->pdo = null;
            throw $e;
        }
    }
}

/**
 * Exception for AndroidAppStats
 */
class AndroidAppStatsException extends Exception {
    public function __construct($message, $code = 0, Exception $previous = null) {
        parent::__construct($message);
    }
}

?>
